Skip to content

Add ProxyJump support for SSH connections#1858

Open
Bnjoroge1 wants to merge 9 commits intogeneralaction:mainfrom
Bnjoroge1:add-jumphost-eo743
Open

Add ProxyJump support for SSH connections#1858
Bnjoroge1 wants to merge 9 commits intogeneralaction:mainfrom
Bnjoroge1:add-jumphost-eo743

Conversation

@Bnjoroge1
Copy link
Copy Markdown

Implements first-class ProxyJump support for SSH connections.

  • Parses \ from \
  • Applies host alias resolution for saved/test SSH connections
  • Uses a local \ proxy socket for jump host transport
  • Adds regression tests for config parsing and proxy-jump failures

Refs #1857

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR adds first-class ProxyJump support for SSH connections by parsing ProxyJump directives from ~/.ssh/config, resolving host aliases in both testConnection and buildConnectConfigFromRow, and wrapping the ssh -W child process in a Duplex socket (ProcessSocket) that ssh2 can use as its transport.

  • P1 — process leak in testConnection: proxySock is not destroyed in the client.on('error') callback (only the synchronous catch block cleans it up). Any SSH-level failure (bad password, key mismatch, timeout) leaves the ssh -W child process running indefinitely.

Confidence Score: 3/5

Not safe to merge as-is; the proxy subprocess leaks on every failed test-connection attempt.

One P1 resource leak (child process not destroyed on SSH client error) keeps the score below the P1 ceiling of 4. The rest of the implementation is well-structured with good test coverage.

src/main/core/ssh/controller.ts — missing proxySock?.destroy() in the client.on('error') handler.

Important Files Changed

Filename Overview
src/main/core/ssh/controller.ts P1: proxySock child process leaks when client.on('error') fires — proxySock?.destroy() is missing from that handler, unlike the synchronous try/catch path which cleans up correctly.
src/main/core/ssh/proxy-jump-sock.ts New file: ProcessSocket Duplex wrapper + buildProxyJumpSocket. Solid backpressure handling; two minor issues: multi-hop ProxyJump silently drops hops beyond the first, and the exit-code error message can read "exit code null" when both code and signal are null.
src/main/core/ssh/build-connect-config.ts Refactored to resolve SSH config host aliases and attach ProxyJump socket after auth config is built; auth failure before socket creation means no resource leak.
src/main/core/ssh/sshConfigParser.ts Adds multi-alias Host parsing, resolveSshConfigHost function, and ProxyJump directive parsing. Logic is clean; ProxyJump none is correctly treated as a no-op.
src/main/core/ssh/utils.ts resolveIdentityAgent now delegates to resolveSshConfigHost; duplicate implementation removed cleanly.
src/shared/ssh.ts Adds proxyJump?: string field to SshConfigHost interface.

Comments Outside Diff (1)

  1. src/main/core/ssh/controller.ts, line 127-130 (link)

    P1 ProxyJump socket leaks on SSH client error

    When client.on('error') fires (e.g., authentication failure, timeout, connection refused), proxySock is resolved away without being explicitly destroyed. The underlying ssh -W ... child process is left running indefinitely until the parent process exits. The try/catch path already calls proxySock?.destroy(), but the async client.on('error') callback has no equivalent cleanup.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/main/core/ssh/controller.ts
    Line: 127-130
    
    Comment:
    **ProxyJump socket leaks on SSH client error**
    
    When `client.on('error')` fires (e.g., authentication failure, timeout, connection refused), `proxySock` is resolved away without being explicitly destroyed. The underlying `ssh -W ...` child process is left running indefinitely until the parent process exits. The `try/catch` path already calls `proxySock?.destroy()`, but the async `client.on('error')` callback has no equivalent cleanup.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/main/core/ssh/controller.ts:127-130
**ProxyJump socket leaks on SSH client error**

When `client.on('error')` fires (e.g., authentication failure, timeout, connection refused), `proxySock` is resolved away without being explicitly destroyed. The underlying `ssh -W ...` child process is left running indefinitely until the parent process exits. The `try/catch` path already calls `proxySock?.destroy()`, but the async `client.on('error')` callback has no equivalent cleanup.

### Issue 2 of 3
src/main/core/ssh/proxy-jump-sock.ts:61
**Multi-hop ProxyJump chain silently truncated**

`ProxyJump` values can be comma-separated to define a multi-hop chain (e.g., `ProxyJump hop1,hop2`). `splitProxyJumpEntry` only takes the first hop — additional hops are silently discarded. Consider using `-J hop1,hop2` when there are multiple entries so the full chain is honoured.

```suggestion
  // NOTE: only the first hop is forwarded to the local ssh binary via -W.
  // Multi-hop ProxyJump (comma-separated) would require passing -J hop1,hop2
  // instead; this is a known limitation of the current implementation.
  const firstHop = proxyJump.split(',')[0]?.trim() ?? '';
```

### Issue 3 of 3
src/main/core/ssh/proxy-jump-sock.ts:139-141
**Ambiguous error message when process exits with neither a code nor a signal**

On some platforms a child process can exit with `code === null` and `signal === null` simultaneously. The current guard skips only `code === 0`, so `null` falls through and the message reads `"ProxyJump command failed (exit code null)"`, which is confusing.

```suggestion
  child.once('exit', (code, signal) => {
    if (sock.destroyed || code === 0) return;
    const reason = signal ? `signal ${signal}` : code != null ? `exit code ${code}` : 'unknown exit';
```

Reviews (1): Last reviewed commit: "ssh: keep proxy jump host destination in..." | Re-trigger Greptile

Comment thread src/main/core/ssh/proxy-jump-sock.ts Outdated
}

function splitProxyJumpEntry(proxyJump: string): { destination: string; port?: string } {
const firstHop = proxyJump.split(',')[0]?.trim() ?? '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Multi-hop ProxyJump chain silently truncated

ProxyJump values can be comma-separated to define a multi-hop chain (e.g., ProxyJump hop1,hop2). splitProxyJumpEntry only takes the first hop — additional hops are silently discarded. Consider using -J hop1,hop2 when there are multiple entries so the full chain is honoured.

Suggested change
const firstHop = proxyJump.split(',')[0]?.trim() ?? '';
// NOTE: only the first hop is forwarded to the local ssh binary via -W.
// Multi-hop ProxyJump (comma-separated) would require passing -J hop1,hop2
// instead; this is a known limitation of the current implementation.
const firstHop = proxyJump.split(',')[0]?.trim() ?? '';
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/core/ssh/proxy-jump-sock.ts
Line: 61

Comment:
**Multi-hop ProxyJump chain silently truncated**

`ProxyJump` values can be comma-separated to define a multi-hop chain (e.g., `ProxyJump hop1,hop2`). `splitProxyJumpEntry` only takes the first hop — additional hops are silently discarded. Consider using `-J hop1,hop2` when there are multiple entries so the full chain is honoured.

```suggestion
  // NOTE: only the first hop is forwarded to the local ssh binary via -W.
  // Multi-hop ProxyJump (comma-separated) would require passing -J hop1,hop2
  // instead; this is a known limitation of the current implementation.
  const firstHop = proxyJump.split(',')[0]?.trim() ?? '';
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/main/core/ssh/proxy-jump-sock.ts Outdated
Comment on lines +139 to +141
child.once('exit', (code, signal) => {
if (sock.destroyed || code === 0) return;
const reason = signal ? `signal ${signal}` : `exit code ${code}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Ambiguous error message when process exits with neither a code nor a signal

On some platforms a child process can exit with code === null and signal === null simultaneously. The current guard skips only code === 0, so null falls through and the message reads "ProxyJump command failed (exit code null)", which is confusing.

Suggested change
child.once('exit', (code, signal) => {
if (sock.destroyed || code === 0) return;
const reason = signal ? `signal ${signal}` : `exit code ${code}`;
child.once('exit', (code, signal) => {
if (sock.destroyed || code === 0) return;
const reason = signal ? `signal ${signal}` : code != null ? `exit code ${code}` : 'unknown exit';
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/core/ssh/proxy-jump-sock.ts
Line: 139-141

Comment:
**Ambiguous error message when process exits with neither a code nor a signal**

On some platforms a child process can exit with `code === null` and `signal === null` simultaneously. The current guard skips only `code === 0`, so `null` falls through and the message reads `"ProxyJump command failed (exit code null)"`, which is confusing.

```suggestion
  child.once('exit', (code, signal) => {
    if (sock.destroyed || code === 0) return;
    const reason = signal ? `signal ${signal}` : code != null ? `exit code ${code}` : 'unknown exit';
```

How can I resolve this? If you propose a fix, please make it concise.

@Bnjoroge1
Copy link
Copy Markdown
Author

Will address Greptile's comments. I was planning on leaving multi-hop impl for a different PR since my main use cases was a single hop but I'll add that.

@arnestrickmann
Copy link
Copy Markdown
Contributor

Hi @Bnjoroge1,
Thanks for opening the PR and for your interest in contributing.
We'll have a look soon.

@Bnjoroge1 Bnjoroge1 force-pushed the add-jumphost-eo743 branch from aabefba to bbe7536 Compare May 4, 2026 17:45
@Bnjoroge1
Copy link
Copy Markdown
Author

nit:

  1. right now the PR assumes you have a jumphost entry/ref in your ssh config. doesnt allow you to add one in emdash's UI - can try to open a separate PR for that, but I imagine most cases where you have some kind of bastion, it's probably defined in your config file so just wanted to keep things simple, and has been my primary way of using it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants